using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Poco;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using TcpServer;
using UnityEngine;
using Debug = UnityEngine.Debug;

public class PocoManager : MonoBehaviour
{
	public const int versionCode = 6;
	public int port = 5001;
	private bool mRunning;
	public AsyncTcpServer server = null;
	private RPCParser rpc = null;
	private SimpleProtocolFilter prot = null;
	private UnityDumper dumper = new UnityDumper();
	private ConcurrentDictionary<string, TcpClientState> inbox = new ConcurrentDictionary<string, TcpClientState>();
	private VRSupport vr_support = new VRSupport();
	private Dictionary<string, long> debugProfilingData = new Dictionary<string, long>() {
		{ "dump", 0 },
		{ "screenshot", 0 },
		{ "handleRpcRequest", 0 },
		{ "packRpcResponse", 0 },
		{ "sendRpcResponse", 0 },
	};

	class RPC : Attribute
	{
	}

	void Awake()
	{
		Application.runInBackground = true;
		DontDestroyOnLoad(this);
		prot = new SimpleProtocolFilter();
		rpc = new RPCParser();
		rpc.addRpcMethod("isVRSupported", vr_support.isVRSupported);
		rpc.addRpcMethod("hasMovementFinished", vr_support.IsQueueEmpty);
		rpc.addRpcMethod("RotateObject", vr_support.RotateObject);
		rpc.addRpcMethod("ObjectLookAt", vr_support.ObjectLookAt);
		rpc.addRpcMethod("Screenshot", Screenshot);
		rpc.addRpcMethod("GetScreenSize", GetScreenSize);
		rpc.addRpcMethod("Dump", Dump);
		rpc.addRpcMethod("GetDebugProfilingData", GetDebugProfilingData);
		rpc.addRpcMethod("SetText", SetText);

		rpc.addRpcMethod("GetSDKVersion", GetSDKVersion);

		mRunning = true;

		for (int i = 0; i < 5; i++) {
			this.server = new AsyncTcpServer (port + i);
			this.server.Encoding = Encoding.UTF8;
			this.server.ClientConnected +=
				new EventHandler<TcpClientConnectedEventArgs> (server_ClientConnected);
			this.server.ClientDisconnected +=
				new EventHandler<TcpClientDisconnectedEventArgs> (server_ClientDisconnected);
			this.server.DatagramReceived +=
				new EventHandler<TcpDatagramReceivedEventArgs<byte[]>> (server_Received);
			try {
				this.server.Start ();
				Debug.Log (string.Format("Tcp server started and listening at {0}", server.Port));
				break;
			} catch (SocketException e) {
				// try next available port
				this.server = null;
			}
		}
		if (this.server == null) {
			Debug.LogError (string.Format("Unable to find an unused port from {0} to {1}", port, port + 5));
		}
		vr_support.ClearCommands();
	}

	static void server_ClientConnected(object sender, TcpClientConnectedEventArgs e)
	{
		Debug.Log(string.Format("TCP client {0} has connected.",
			e.TcpClient.Client.RemoteEndPoint.ToString()));
	}

	static void server_ClientDisconnected(object sender, TcpClientDisconnectedEventArgs e)
	{
		Debug.Log(string.Format("TCP client {0} has disconnected.",
		   e.TcpClient.Client.RemoteEndPoint.ToString()));
	}

	private void server_Received(object sender, TcpDatagramReceivedEventArgs<byte[]> e)
	{
		Debug.Log(string.Format("Client : {0} --> {1}",
			e.Client.TcpClient.Client.RemoteEndPoint.ToString(), e.Datagram.Length));
		TcpClientState internalClient = e.Client;
		string tcpClientKey = internalClient.TcpClient.Client.RemoteEndPoint.ToString();
		inbox.AddOrUpdate(tcpClientKey, internalClient, (n, o) =>
		{
			return internalClient;
		});
	}

	[RPC]
	private object Dump(List<object> param)
	{
		var onlyVisibleNode = true;
		if (param.Count > 0)
		{
			onlyVisibleNode = (bool)param[0];
		}
		var sw = new Stopwatch();
		sw.Start();
		var h = dumper.dumpHierarchy(onlyVisibleNode);
		debugProfilingData["dump"] = sw.ElapsedMilliseconds;

		return h;
	}

	[RPC]
	private object Screenshot(List<object> param)
	{
		var sw = new Stopwatch();
		sw.Start();

		var tex = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
		tex.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
		tex.Apply(false);
		byte[] fileBytes = tex.EncodeToJPG(80);
		var b64img = Convert.ToBase64String(fileBytes);
		debugProfilingData["screenshot"] = sw.ElapsedMilliseconds;
		return new object[] { b64img, "jpg" };
	}

	[RPC]
	private object GetScreenSize(List<object> param)
	{
		return new float[] { Screen.width, Screen.height };
	}

	public void stopListening()
	{
		mRunning = false;
		server.Stop();
	}

	[RPC]
	private object GetDebugProfilingData(List<object> param)
	{
		return debugProfilingData;
	}

	[RPC]
	private object SetText(List<object> param)
	{
		var instanceId = Convert.ToInt32(param[0]);
		var textVal = param[1] as string;
		foreach (var go in GameObject.FindObjectsOfType<GameObject>())
		{
			if (go.GetInstanceID() == instanceId)
			{
				return UnityNode.SetText(go, textVal);
			}
		}
		return false;
	}

	[RPC]
	private object GetSDKVersion(List<object> param)
	{
		return versionCode;
	}

	void Update()
	{
		foreach (TcpClientState client in inbox.Values)
		{
			List<string> msgs = client.Prot.swap_msgs();
			msgs.ForEach(delegate (string msg)
			{
				var sw = new Stopwatch();
				sw.Start();
				var t0 = sw.ElapsedMilliseconds;
				string response = rpc.HandleMessage(msg);
				var t1 = sw.ElapsedMilliseconds;
				byte[] bytes = prot.pack(response);
				var t2 = sw.ElapsedMilliseconds;
				server.Send(client.TcpClient, bytes);
				var t3 = sw.ElapsedMilliseconds;
				debugProfilingData["handleRpcRequest"] = t1 - t0;
				debugProfilingData["packRpcResponse"] = t2 - t1;
				TcpClientState internalClientToBeThrowAway;
				string tcpClientKey = client.TcpClient.Client.RemoteEndPoint.ToString();
				inbox.TryRemove(tcpClientKey, out internalClientToBeThrowAway);
			});
		}

		vr_support.PeekCommand();
	}

	void OnApplicationQuit()
	{
	    // stop listening thread
		stopListening();
	}
}


public class RPCParser
{
	public delegate object RpcMethod(List<object> param);

	protected Dictionary<string, RpcMethod> RPCHandler = new Dictionary<string, RpcMethod>();
	private JsonSerializerSettings settings = new JsonSerializerSettings()
	{
		StringEscapeHandling = StringEscapeHandling.EscapeNonAscii
	};

	public string HandleMessage(string json)
	{
		Dictionary<string, object> data = JsonConvert.DeserializeObject<Dictionary<string, object>>(json, settings);
		if (data.ContainsKey("method"))
		{
			string method = data["method"].ToString();
			List<object> param = null;
			if (data.ContainsKey("params"))
			{
				param = ((JArray)(data["params"])).ToObject<List<object>>();
			}

			object idAction = null;
			if (data.ContainsKey("id"))
			{
				// if it have id, it is a request
				idAction = data["id"];
			}

			string response = null;
			object result = null;
			try
			{
				result = RPCHandler[method](param);
			}
			catch (Exception e)
			{
				// return error response
				Debug.Log(e);
				response = formatResponseError(idAction, null, e);
				return response;
			}

			// return result response
			response = formatResponse(idAction, result);
			return response;

		}
		else
		{
			// do not handle response
			Debug.Log("ignore message without method");
			return null;
		}
	}

	// Call a method in the server
	public string formatRequest(string method, object idAction, List<object> param = null)
	{
		Dictionary<string, object> data = new Dictionary<string, object>();
		data["jsonrpc"] = "2.0";
		data["method"] = method;
		if (param != null)
		{
			data["params"] = JsonConvert.SerializeObject(param, settings);
		}
		// if idAction is null, it is a notification
		if (idAction != null)
		{
			data["id"] = idAction;
		}
		return JsonConvert.SerializeObject(data, settings);
	}

	// Send a response from a request the server made to this client
	public string formatResponse(object idAction, object result)
	{
		Dictionary<string, object> rpc = new Dictionary<string, object>();
		rpc["jsonrpc"] = "2.0";
		rpc["id"] = idAction;
		rpc["result"] = result;
		return JsonConvert.SerializeObject(rpc, settings);
	}

	// Send a error to the server from a request it made to this client
	public string formatResponseError(object idAction, IDictionary<string, object> data, Exception e)
	{
		Dictionary<string, object> rpc = new Dictionary<string, object>();
		rpc["jsonrpc"] = "2.0";
		rpc["id"] = idAction;

		Dictionary<string, object> errorDefinition = new Dictionary<string, object>();
		errorDefinition["code"] = 1;
		errorDefinition["message"] = e.ToString();

		if (data != null)
		{
			errorDefinition["data"] = data;
		}

		rpc["error"] = errorDefinition;
		return JsonConvert.SerializeObject(rpc, settings);
	}

	public void addRpcMethod(string name, RpcMethod method)
	{
		RPCHandler[name] = method;
	}
}
